﻿/* Copyright © 2010 Nolics
Nolics.net and NSQLObjects are trademarks of Nolics.

Microsoft Public License (Ms-PL)

This license governs use of the accompanying software. If you use the software, you accept this license. If you do not accept the license, do not use the software.

1. Definitions
The terms "reproduce," "reproduction," "derivative works," and "distribution" have the same meaning here as under U.S. copyright law.
A "contribution" is the original software, or any additions or changes to the software.
A "contributor" is any person that distributes its contribution under this license.
"Licensed patents" are a contributor's patent claims that read directly on its contribution.

2. Grant of Rights
(A) Copyright Grant- Subject to the terms of this license, including the license conditions and limitations in section 3, each contributor grants you a non-exclusive, worldwide, royalty-free copyright license to reproduce its contribution, prepare derivative works of its contribution, and distribute its contribution or any derivative works that you create.
(B) Patent Grant- Subject to the terms of this license, including the license conditions and limitations in section 3, each contributor grants you a non-exclusive, worldwide, royalty-free license under its licensed patents to make, have made, use, sell, offer for sale, import, and/or otherwise dispose of its contribution in the software or derivative works of the contribution in the software.

3. Conditions and Limitations
(A) No Trademark License- This license does not grant you rights to use any contributors' name, logo, or trademarks.
(B) If you bring a patent claim against any contributor over patents that you claim are infringed by the software, your patent license from such contributor to the software ends automatically.
(C) If you distribute any portion of the software, you must retain all copyright, patent, trademark, and attribution notices that are present in the software.
(D) If you distribute any portion of the software in source code form, you may do so only under this license by including a complete copy of this license with your distribution. If you distribute any portion of the software in compiled or object code form, you may only do so under a license that complies with this license.
(E) The software is licensed "as-is." You bear the risk of using it. The contributors give no express warranties, guarantees or conditions. You may have additional consumer rights under your local laws which this license cannot change. To the extent permitted under your local laws, the contributors exclude the implied warranties of merchantability, fitness for a particular purpose and non-infringement. 
 */

using System;
using System.Collections.Generic;
using System.Data.Common;
using System.Data.SqlClient;
using System.Text;
using NSQLObjects.Exceptions;
using NSQLObjects.Model;

namespace NSQLObjects.Persistence
{
    public abstract class SqlDataContext : IDisposable
    {
        public SqlDataContext(SqlConnection connection) {
            this.connection = connection;
        }

        /// <summary>
        /// Create sql context using SQLContextFactory
        /// </summary>
        /// <returns></returns>
        public static SqlDataContext CreateContext() {
            return SQLContextFactory.ContextFactory.CreateContext();
        }

        /// <summary>
        /// Set default connection string
        /// </summary>
        /// <param name="connectionString"></param>
        public static void SetDefaultConnection(string connectionString) {
            SQLContextFactory.ContextFactory = new SQLContextFactory(connectionString);
        }

        /// <summary>
        /// Test if context is alive (not disposed)
        /// </summary>
        /// <returns></returns>
        public bool IsAlive() {
            return connection != null;
        }

        public SqlConnection Connection {
            get { return connection; }
        }

        protected SqlConnection connection;

        /// <summary>
        /// Create and load query or fetch
        /// </summary>
        /// <typeparam name="T">Query type</typeparam>
        /// <param name="setup">Optional query setup action</param>
        /// <returns>Loaded query</returns>
        public T Load<T>(Action<T> setup = null) where T : IQueryLoad, new() {
            T query = new T();
            if (setup != null)
                setup(query);
            query.LoadQuery(this);
            return query;
        }

        /// <summary>
        /// Run query commmand and append results  to query
        /// </summary>
        /// <remarks>Do not call this method directly. It's called from indirectly from your Queries</remarks>
        /// <param name="queryCommand">Query command</param>
        /// <param name="query">Query to receive resulting rows</param>
        public void FillQuery<T>(SqlCommand queryCommand, IQueryFill<T> query) where T : IRow {
            queryCommand.Connection = connection;
            queryCommand.Transaction = GetTransaction();
            List<IRowLoaded> loadedRows = new List<IRowLoaded>();

            using (SqlDataReader reader = queryCommand.ExecuteReader()) {
                while (reader.Read()) {
                    IRow newRow = query.AddRow();
                    newRow.FillRow(reader);
                    ITableRow tableRow = newRow as ITableRow;
                    if (tableRow != null)
                        Attach(tableRow);
                    IRowLoaded rl = newRow as IRowLoaded;
                    if (rl != null)
                        loadedRows.Add(rl);
                }
            }
            loadedRows.ForEach(rl => rl.Loaded(this));
        }



        /// <summary>
        /// Run query commmand and return single integer from result. Query must contain count(*) itself.
        /// </summary>
        /// <remarks>Do not call this method directly. It's called from indirectly from your Queries</remarks>
        /// <param name="queryCommand">Query command</param>
        /// <param name="query">Query to receive resulting rows</param>
        public int LoadQueryCount(SqlCommand queryCommand) {
            queryCommand.Connection = connection;
            queryCommand.Transaction = GetTransaction();
            return Convert.ToInt32(queryCommand.ExecuteScalar());
        }

        /// <summary>
        /// Submit all changes to database
        /// </summary>
        /// <param name="state">Optional user state object</param>
        public virtual void SubmitChanges(object state = null) {
            CommitContext cc = new CommitContext(this, state);
            ValidateChanges(cc);
            try {
                BeginTransaction();
                FireBeforeAfterChanges(false, cc);
                modifiedEntries.ForEach(UpdateChanges);
                FireBeforeAfterChanges(true, cc);
                EndTransaction(true);

                modifiedEntries.ForEach(ClearStates);
                modifiedEntries = new List<TrackInformation>();
            } catch {
                EndTransaction(false);
                throw;
            }
        }

        protected abstract SqlTransaction GetTransaction();

        protected abstract SqlTransaction BeginTransaction();

        protected abstract void EndTransaction(bool fCommit);

        /// <summary>
        /// This method is called before and after modified items have been updated, inserted or deleted from database
        /// </summary>
        /// <remarks>Default implementation will invoke ITableRowCommit methods on object that implement interface. Both call's will occur while an update transaction
        /// is active.</remarks>
        /// <param name="fAfter">Call after changes</param>
        protected virtual void FireBeforeAfterChanges(bool fAfter, ICommitContext context) {
            foreach (var tr in modifiedEntries) {
                ITableRowCommit iCommit = tr.Values as ITableRowCommit;
                if (iCommit != null) {
                    if (fAfter)
                        iCommit.AfterDatabaseChanges(tr.State, tr.OriginalValues, context);
                    else
                        iCommit.BeforeDatabaseChanges(tr.State, tr.OriginalValues, context);
                }
            }
        }

        /// <summary>
        /// 
        /// </summary>
        protected virtual void ValidateChanges(ICommitContext context) {
            foreach (var tr in modifiedEntries) {
                ITableRowValidate validate = tr.Values as ITableRowValidate;
                if (validate != null)
                    validate.ValidateRow(tr.State, tr.OriginalValues, context);
            }
        }


        private void UpdateChanges(TrackInformation tr) {
            switch (tr.State) {
                case RowStates.Unchanged:
                    return; // This should not normally happend
                case RowStates.Modified:
                    ModifyRow(tr);
                    return;
                case RowStates.New:
                    InsertRow(tr);
                    return;
                case RowStates.Deleted:
                    DeleteRow(tr);
                    return;
            }
        }

        private void ClearStates(TrackInformation tr) {
            if (tr.State == RowStates.Deleted)
                trackedRows.Remove(tr.Values);
            else
                trackedRows[tr.Values] = null;
        }

        private void DeleteRow(TrackInformation tr) {
            SqlCommand deleteCommand = CreateCommand();
            StringBuilder sb = new StringBuilder();
            sb.Append("delete from ");
            sb.Append(tr.Values.TargetTable);
            sb.Append(" where ");
            ParameterList plWhere = new ParameterList();
            tr.OriginalValues.GetParameters(ParameterSets.DeleteCompares, plWhere);
            bool fFirst = true;
            foreach (DbParameter par in plWhere) {
                if (!fFirst)
                    sb.Append(" and ");
                else
                    fFirst = false;
                sb.Append("[");
                sb.Append(par.ParameterName);
                sb.Append("] = ");
                FormulaParameter fp = par as FormulaParameter;
                if (fp != null) {
                    sb.Append(fp.Formula);
                    continue;
                }
                sb.Append("@");
                sb.Append(par.ParameterName);
                sb.Append(" ");
                deleteCommand.Parameters.Add((SqlParameter)par);
            }
            deleteCommand.CommandText = sb.ToString();
            if (deleteCommand.ExecuteNonQuery() != 1)
                throw new UpdateConflictException("Row delete failed");
        }

        private void InsertRow(TrackInformation tr) {
            SqlCommand insertCommand = CreateCommand();
            StringBuilder sb = new StringBuilder();
            sb.Append("insert into ");
            sb.Append(tr.Values.TargetTable);
            sb.Append("(");
            ParameterList plInsert = new ParameterList();
            tr.Values.GetParameters(ParameterSets.InsertValues, plInsert);
            bool fFirst = true;
            foreach (DbParameter par in plInsert) {
                if (!fFirst)
                    sb.Append(", ");
                else
                    fFirst = false;
                sb.Append("[");
                sb.Append(par.ParameterName);
                sb.Append("]");
            }

            sb.Append(") values (");
            fFirst = true;
            foreach (DbParameter par in plInsert) {
                if (!fFirst)
                    sb.Append(", ");
                else
                    fFirst = false;
                FormulaParameter fp = par as FormulaParameter;
                if (fp != null) {
                    sb.Append(fp.Formula);
                    continue;
                }
                sb.Append("@");
                sb.Append(par.ParameterName);
                sb.Append(" ");
                insertCommand.Parameters.Add((SqlParameter)par);
            }
            sb.Append(")");
            insertCommand.CommandText = sb.ToString();
            insertCommand.ExecuteNonQuery();
            AssignNewID(tr);
        }

        private void AssignNewID(TrackInformation tr) {
            IEntityCreated iec = tr.Values as IEntityCreated;
            if (iec == null)
                return;
            using (SqlCommand queryID = CreateCommand()) {
                queryID.CommandText = "select @@identity";
                long newId = Convert.ToInt64(queryID.ExecuteScalar());
                iec.SetIdentity(newId);
            }
        }

        private void ModifyRow(TrackInformation tr) {
            SqlCommand updateCommand = CreateCommand();
            StringBuilder sb = new StringBuilder();
            sb.Append("update ");
            sb.Append(tr.Values.TargetTable);
            sb.Append(" set ");
            ParameterList plUpdate = new ParameterList();
            tr.Values.GetParameters(ParameterSets.UpdateValues, plUpdate);
            bool fFirst = true;
            foreach (DbParameter par in plUpdate) {
                if (!fFirst)
                    sb.Append(", ");
                else
                    fFirst = false;
                sb.Append("[");
                sb.Append(par.ParameterName);
                sb.Append("] = ");
                FormulaParameter fp = par as FormulaParameter;
                if (fp != null) {
                    sb.Append(fp.Formula);
                    continue;
                }
                sb.Append("@");
                sb.Append(par.ParameterName);
                sb.Append(" ");
                updateCommand.Parameters.Add((SqlParameter)par);
            }
            sb.Append(" where ");
            ParameterList plWhere = new ParameterList();
            tr.OriginalValues.GetParameters(ParameterSets.UpdateCompares, plWhere);
            fFirst = true;
            foreach (DbParameter par in plWhere) {
                if (!fFirst)
                    sb.Append(" and ");
                else
                    fFirst = false;
                sb.Append("[");
                sb.Append(par.ParameterName);
                if (par is SqlParameter && par.Value is DBNull) {
                    sb.Append("] is null ");
                } else {
                    sb.Append("] = ");
                    FormulaParameter fp = par as FormulaParameter;
                    if (fp != null) {
                        sb.Append(fp.Formula);
                        continue;
                    }
                    par.ParameterName = "old__" + par.ParameterName;
                    sb.Append("@");
                    sb.Append(par.ParameterName);
                    sb.Append(" ");
                    updateCommand.Parameters.Add((SqlParameter)par);
                }
            }
            updateCommand.CommandText = sb.ToString();
            if (updateCommand.ExecuteNonQuery() != 1)
                throw new UpdateConflictException("Row update failed");
        }

        /// <summary>
        /// Retrieve row state
        /// </summary>
        /// <param name="row">Table row to check</param>
        /// <returns></returns>
        public RowStates GetRowState(ITableRow row) {
            if (!trackedRows.ContainsKey(row))
                return RowStates.Untracked;
            TrackInformation tr = trackedRows[row];
            if (tr != null)
                return tr.State;
            return RowStates.Unchanged;
        }

        /// <summary>
        /// List all changed rows
        /// </summary>
        /// <returns></returns>
        public List<ITableRow> GetChangedRows() {
            var list = new List<ITableRow>(modifiedEntries.Count);
            modifiedEntries.ForEach(tr => list.Add(tr.Values));
            return list;
        }

        /// <summary>
        /// Mark's row as deleted. Actual delete will not be performed before changes are submitted
        /// </summary>
        /// <param name="row"></param>
        public void Delete(ITableRow row) {
            TrackInformation tr = trackedRows[row];
            if (tr != null) {
                if (tr.State == RowStates.New) {
                    modifiedEntries.Remove(tr);
                    trackedRows.Remove(row);
                    return;
                }
                tr.State = RowStates.Deleted;
                return;
            }
            CreateTrackInformation(row).State = RowStates.Deleted;
        }

        /// <summary>
        /// Mark's row as new. Row will be inserted when changes are submitted
        /// </summary>
        /// <param name="row"></param>
        public void Insert(ITableRow row) {
            if (trackedRows.ContainsKey(row)) {
                throw new SequenceException("Row is already tracked");
            }
            trackedRows.Add(row, null);
            CreateTrackInformation(row).State = RowStates.New;
        }

        /// <summary>
        /// Attach row to data context
        /// </summary>
        /// <param name="row"></param>
        public void Attach(ITableRow row) {
            if (trackedRows.ContainsKey(row))
                throw new SequenceException("Row is already tracked");
            trackedRows.Add(row, null);
            row.PersistedPropertyChanging += new PersistedPropertyChangingHandler(row_PropertyChangeing);
        }

        /// <summary>
        /// Attach row to data context
        /// </summary>
        /// <param name="row"></param>
        public void Attach(ITableRow row, ITableRow originalValues) {
            Attach(row);
            var tr = new TrackInformation();
            tr.Values = row;
            tr.OriginalValues = originalValues;
            tr.State = RowStates.Modified;
            trackedRows[row] = tr;
            modifiedEntries.Add(tr);
        }

        /// <summary>
        /// Mark that row has changed
        /// </summary>
        /// <param name="row"></param>
        public void Modify(ITableRow row) {
            TrackInformation tr = CreateTrackInformation(row);
            if (tr.State == RowStates.Unchanged)
                tr.State = RowStates.Modified;
            if (tr.State == RowStates.Deleted)
                throw new SequenceException("Row is marked for deletion!");
        }

        /// <summary>
        /// Detach row from contexts
        /// </summary>
        /// <param name="row"></param>
        public void Detach(ITableRow row) {
            if (trackedRows.ContainsKey(row)) {
                TrackInformation tr = trackedRows[row];
                if (tr != null)
                    modifiedEntries.Remove(tr);
                trackedRows.Remove(row);
                row.PersistedPropertyChanging -= new PersistedPropertyChangingHandler(row_PropertyChangeing);
            }
        }

        protected virtual void row_PropertyChangeing(ITableRow row, string propertyName) {
            Modify(row);
        }

        /// <summary>
        /// Creates change track information if it is not already created
        /// </summary>
        /// <param name="row">Row to track</param>
        /// <returns></returns>
        private TrackInformation CreateTrackInformation(ITableRow row) {
            if (!trackedRows.ContainsKey(row))
                throw new SequenceException("Row is not attached to this data context");
            TrackInformation tr = trackedRows[row];
            if (tr == null) {
                tr = new TrackInformation();
                tr.Values = row;
                tr.OriginalValues = row.Clone();
                tr.State = RowStates.Unchanged;
                trackedRows[row] = tr;
                modifiedEntries.Add(tr);
            }
            return tr;
        }

        protected class TrackInformation
        {
            public RowStates State { get; set; }

            /// <summary>
            /// Current values
            /// </summary>
            public ITableRow Values { get; set; }

            public ITableRow OriginalValues { get; set; }
        }

        private Dictionary<ITableRow, TrackInformation> trackedRows = new Dictionary<ITableRow, TrackInformation>();
        private List<TrackInformation> modifiedEntries = new List<TrackInformation>();

        #region IDisposable Members

        public virtual void Dispose() {
            connection = null;
        }

        #endregion

        #region IExecuteProcedure Members

        public void ExecuteProcedure(string procName, ParameterList parameters) {
            SqlCommand cmd = CreateCommand();
            cmd.CommandType = System.Data.CommandType.StoredProcedure;
            cmd.CommandText = procName;
            parameters.ForEach(par => cmd.Parameters.Add((SqlParameter)par));
            cmd.ExecuteNonQuery();
        }

        #endregion

        private SqlCommand CreateCommand() {
            SqlCommand cmd = new SqlCommand();
            cmd.Connection = connection;
            cmd.Transaction = GetTransaction();
            return cmd;
        }

        protected class CommitContext : ICommitContext
        {
            public CommitContext(SqlDataContext parent, object state) {
                this.CommitState = state;
                this.Parent = parent;
            }

            public SqlDataContext Parent { get; private set; }

            #region ICommitContext Members

            public void ExecuteProcedure(string procName, ParameterList parameters) {
                Parent.ExecuteProcedure(procName, parameters);
            }

            public object CommitState { get; private set; }

            #endregion
        }

    }

    public class StandaloneSqlDataContext : SqlDataContext
    {

        public StandaloneSqlDataContext(SqlConnection connection)
            : base(connection) {
        }

        public SqlTransaction ActiveTransaction() {
            return BeginTransaction();
        }

        private SqlTransaction tran;
        protected override SqlTransaction GetTransaction() {
            return tran;
        }

        protected override SqlTransaction BeginTransaction() {
            if (tran == null)
                tran = connection.BeginTransaction();
            return tran;
        }

        protected override void EndTransaction(bool fCommit) {
            if (tran != null) {
                if (fCommit)
                    tran.Commit();
                else
                    tran.Rollback();
            }
            tran = null;
        }

        public override void Dispose() {
            if (connection != null)
                connection.Close();
            base.Dispose();
        }
    }
}
